{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# General Mixture Models"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "author: Jacob Schreiber <br>\n",
    "contact: jmschreiber91@gmail.com"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It is frequently the case that the data you have is not explained by a single underlying distribution. If we want to try to recover the underlying distributions, we need to have a model which has multiple components. An example is the following data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Populating the interactive namespace from numpy and matplotlib\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING: pylab import has clobbered these variables: ['random', 'log']\n",
      "`%matplotlib` prevents importing * from pylab and numpy\n"
     ]
    }
   ],
   "source": [
    "from pomegranate import *\n",
    "%pylab inline\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(array([   1.,    0.,    0.,   10.,   13.,   28.,   26.,   30.,   38.,\n",
       "          28.,   21.,   21.,   21.,   20.,   85.,  149.,  151.,   77.,\n",
       "          27.,    4.]),\n",
       " array([ -7.30907852,  -6.36311627,  -5.41715401,  -4.47119176,\n",
       "         -3.5252295 ,  -2.57926725,  -1.633305  ,  -0.68734274,\n",
       "          0.25861951,   1.20458177,   2.15054402,   3.09650628,\n",
       "          4.04246853,   4.98843078,   5.93439304,   6.88035529,\n",
       "          7.82631755,   8.7722798 ,   9.71824206,  10.66420431,  11.61016656]),\n",
       " <a list of 20 Patch objects>)"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<matplotlib.figure.Figure at 0x7f11efbeced0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "data = np.concatenate( (np.random.randn(250, 1) * 2.75 + 1.25, np.random.randn(500, 1) * 1.2 + 7.85) )\n",
    "np.random.shuffle(data)\n",
    "plt.hist( data, edgecolor='c', color='c', bins=20 )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can create our initial estimate of what this distribution is a General Mixture Model. This is a model which is comprised of multiple distributions, and weights on those distributions representing the prior probability of a point falling under that distribution given no knowledge of the point itself (defaults to equal). We can have univariate mixture models by using univariate distributions, or multivariate distributions by using multivariate distributions."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Gaussian Mixture Models"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "d = GeneralMixtureModel( [NormalDistribution(2.5, 1), NormalDistribution(8, 1)] )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now predict the class labels of each point under this mixture. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1 1 1 1 0]\n",
      "507 1 labels, 243 0 labels\n"
     ]
    }
   ],
   "source": [
    "labels = d.predict( data )\n",
    "print labels[:5]\n",
    "print \"{} 1 labels, {} 0 labels\".format( labels.sum(), labels.shape[0] - labels.sum() )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is fairly close to the number of underlying points from each distribution, off by 17 in each label. We still don't know if the labels are accurate, just the number of labels."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(array([  9.,   8.,  20.,  29.,  36.,  48.,  46.,  54.,  61.,  53.,  40.,\n",
       "         35.,  21.,  16.,  15.,  11.,   1.,   2.,   1.,   1.]),\n",
       " array([  5.33097483,   5.64493442,   5.95889401,   6.27285359,\n",
       "          6.58681318,   6.90077277,   7.21473235,   7.52869194,\n",
       "          7.84265153,   8.15661111,   8.4705707 ,   8.78453029,\n",
       "          9.09848987,   9.41244946,   9.72640904,  10.04036863,\n",
       "         10.35432822,  10.6682878 ,  10.98224739,  11.29620698,  11.61016656]),\n",
       " <a list of 20 Patch objects>)"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<matplotlib.figure.Figure at 0x7f11f2bcd710>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.hist( data[ labels == 0 ], edgecolor='r', color='r', bins=20 )\n",
    "plt.hist( data[ labels == 1 ], edgecolor='c', color='c', bins=20 )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It is slightly more difficult to update the underlying components of the model because we don't have labels indicating which point came from which distribution. We could try to use the labels inferred from the model. It seems to cleanly split it, but what if our initial estimate was not very good? It could be difficult to get a good update if we had a bad prior. \n",
    "\n",
    "Another possibility is to predict the probability of each point under each component, to get a softer estimate of the labels. Lets take a look."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[  1.28630159e-08   9.99999987e-01]\n",
      " [  3.14180327e-10   1.00000000e+00]\n",
      " [  1.28508601e-04   9.99871491e-01]\n",
      " [  5.37380480e-07   9.99999463e-01]\n",
      " [  9.99999998e-01   2.39509083e-09]]\n",
      "[ 242.73703674  507.26296326]\n"
     ]
    }
   ],
   "source": [
    "labels = d.predict_proba( data )\n",
    "print labels[:5]\n",
    "print labels.sum(axis=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is slightly closer to the truth, with 15.2 off instead of 17, around 10% closer.\n",
    "\n",
    "This is the beginning of a common unsupervised training algorithm called <b>expectation maximization</b>. It has two steps, <b>expectation</b> and <b>maximization</b>. The <b>expectation</b> step involves what we just did--assigning weights based on the probability of each point being generated by each component. The next step, <b>maximization</b>, is maximizing the probability that the distribution generated these points but performing weighted MLE.\n",
    "\n",
    "This process must be iterated until convergence. Sometimes this requires only a single update, but for overlapping distributions (such as this one) it can sometimes take many iterations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Improvement: 892.969216325\n",
      "Improvement: 0.786696085199\n",
      "Improvement: 0.0368066512267\n",
      "Total Improvement: 893.792719061\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "893.7927190612211"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "d.fit( data, verbose=True )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Multivariate Gaussian Mixture Models"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can do the same with multivariate distributions just as easily."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "mu = np.arange(5)\n",
    "cov = np.eye(5)\n",
    "\n",
    "mgs = [ MultivariateGaussianDistribution( mu*i, cov ) for i in range(5) ]\n",
    "gmm = GeneralMixtureModel( mgs )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "data = numpy.random.randn(1000, 5) * 5\n",
    "for i in range(5):\n",
    "    data[i::5] += np.arange(5)*i"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lets see how well some points fit under the mixture model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Point 0: logp -92.9968217129\n",
      "Point 1: logp -95.9044220626\n",
      "Point 2: logp -19.5299139507\n",
      "Point 3: logp -28.2330753263\n",
      "Point 4: logp -30.5658500441\n",
      "Point 5: logp -109.57535738\n",
      "Point 6: logp -54.4032830472\n",
      "Point 7: logp -204.647149088\n",
      "Point 8: logp -228.971188173\n",
      "Point 9: logp -52.1138274966\n"
     ]
    }
   ],
   "source": [
    "for i in range(10):\n",
    "    print \"Point {}: logp {}\".format( i, gmm.log_probability(data[i]) )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Improvement: 44541.6683755\n",
      "Improvement: 30.6096878561\n",
      "Improvement: 11.5923550805\n",
      "Improvement: 6.24301245829\n",
      "Improvement: 3.90149691583\n",
      "Improvement: 2.65858549903\n",
      "Improvement: 1.93523543866\n",
      "Improvement: 1.50763034672\n",
      "Improvement: 1.25218891462\n",
      "Improvement: 1.0923188329\n",
      "Improvement: 0.986870608411\n",
      "Total Improvement: 44603.4477575\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "44603.44775749489"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "gmm.fit(data, verbose=True, stop_threshold=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now lets see how well the previous points fit."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Point 0: logp -15.7580845989\n",
      "Point 1: logp -16.114044174\n",
      "Point 2: logp -14.0475691373\n",
      "Point 3: logp -14.7997614439\n",
      "Point 4: logp -14.6995141746\n",
      "Point 5: logp -17.9272784665\n",
      "Point 6: logp -15.3165691235\n",
      "Point 7: logp -20.1189037538\n",
      "Point 8: logp -22.3676868448\n",
      "Point 9: logp -15.9103605907\n"
     ]
    }
   ],
   "source": [
    "for i in range(10):\n",
    "    print \"Point {}: logp {}\".format( i, gmm.log_probability(data[i]) )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looks like they're being fit significantly better than before! Training works."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Discrete Mixture Models"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In addition to having general mixture models over continuous distributions, we can also have mixture models over discrete distributions. This is useful in many bioinformatics contexts, specifically sequence analysis. Lets use the toy analysis of trying to analyze CG island distribution.\n",
    "\n",
    "The problem is the following; DNA is made up of long sequences the four canonical nucleotides, abbreviated 'A', 'C', 'G', and 'T'. These nucleotides are not distributed randomly, and there is significant amounts of structure in the sequence. A major field in bioinformatics is trying to interpret this structure. One structured element is the CG content, where the Cs and the Gs appear more commonly than in the background. If we want to try to determine CG percentages in these islands, we can use a mixture model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "d1 = DiscreteDistribution( {'A' : 0.25, 'C': 0.25, 'G' : 0.25, 'T': 0.25 } ) # Background\n",
    "d2 = DiscreteDistribution( {'A' : 0.05, 'C': 0.45, 'G' : 0.45, 'T': 0.05 } ) # CG rich regions\n",
    "gmm = GeneralMixtureModel( [d1, d2] )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3.4766541840931495"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "seq = numpy.array(list('CGACATCTGACTACGGCGCGCCTACTACTTGATCGATACGGCGTCAGCGACGACGATGATCGGCATCAGTCACTAC'))\n",
    "gmm.fit(seq)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ {\n",
      "    \"frozen\" :false,\n",
      "    \"class\" :\"Distribution\",\n",
      "    \"parameters\" :[\n",
      "        {\n",
      "            \"A\" :0.3500000000000001,\n",
      "            \"C\" :0.19999999999999993,\n",
      "            \"T\" :0.29166666666666674,\n",
      "            \"G\" :0.15833333333333327\n",
      "        }\n",
      "    ],\n",
      "    \"name\" :\"DiscreteDistribution\"\n",
      "}\n",
      " {\n",
      "    \"frozen\" :false,\n",
      "    \"class\" :\"Distribution\",\n",
      "    \"parameters\" :[\n",
      "        {\n",
      "            \"A\" :0.0905172413793103,\n",
      "            \"C\" :0.46551724137931033,\n",
      "            \"T\" :0.07543103448275859,\n",
      "            \"G\" :0.3685344827586208\n",
      "        }\n",
      "    ],\n",
      "    \"name\" :\"DiscreteDistribution\"\n",
      "}]\n",
      "\n",
      "[ 0.56390977  0.43609023]\n"
     ]
    }
   ],
   "source": [
    "print gmm.distributions\n",
    "print\n",
    "print numpy.exp(gmm.weights)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looks like in this case the concept was sound, that there many CG rich regions, but our initial estimates of the  percentages were off. We can use a GMM like the one above to both identify and study the composition of these regions at the same time, updating the parameters of the distributions using expectation-maximization. We will go into a more complex way of dong this using HMMs in the next tutorial."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Serialization"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "General Mixture Models support serialization to JSONs using `to_json()` and `from_json( json )`. This is useful is you want to train a GMM on large amounts of data, taking a significant amount of time, and then use this model in the future without having to repeat this computationally intensive step."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\n",
      "    \"weights\" : [\n",
      "        -0.5728610146854433,\n",
      "        -0.8299061176753344\n",
      "    ],\n",
      "    \"distributions\" : [\n",
      "        {\n",
      "            \"frozen\" : false,\n",
      "            \"class\" : \"Distribution\",\n",
      "            \"parameters\" : [\n",
      "                {\n",
      "                    \"A\" : 0.3500000000000001,\n",
      "                    \"C\" : 0.19999999999999993,\n",
      "                    \"T\" : 0.29166666666666674,\n",
      "                    \"G\" : 0.15833333333333327\n",
      "                }\n",
      "            ],\n",
      "            \"name\" : \"DiscreteDistribution\"\n",
      "        },\n",
      "        {\n",
      "            \"frozen\" : false,\n",
      "            \"class\" : \"Distribution\",\n",
      "            \"parameters\" : [\n",
      "                {\n",
      "                    \"A\" : 0.0905172413793103,\n",
      "                    \"C\" : 0.46551724137931033,\n",
      "                    \"T\" : 0.07543103448275859,\n",
      "                    \"G\" : 0.3685344827586208\n",
      "                }\n",
      "            ],\n",
      "            \"name\" : \"DiscreteDistribution\"\n",
      "        }\n",
      "    ],\n",
      "    \"class\" : \"GeneralMixtureModel\"\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "print gmm.to_json()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Not the prettiest thing to look at right now. However, we can easily load it and use it immediately."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([ {\n",
       "    \"frozen\" :false,\n",
       "    \"class\" :\"Distribution\",\n",
       "    \"parameters\" :[\n",
       "        {\n",
       "            \"A\" :0.3500000000000001,\n",
       "            \"C\" :0.19999999999999993,\n",
       "            \"T\" :0.29166666666666674,\n",
       "            \"G\" :0.15833333333333327\n",
       "        }\n",
       "    ],\n",
       "    \"name\" :\"DiscreteDistribution\"\n",
       "},\n",
       "       {\n",
       "    \"frozen\" :false,\n",
       "    \"class\" :\"Distribution\",\n",
       "    \"parameters\" :[\n",
       "        {\n",
       "            \"A\" :0.0905172413793103,\n",
       "            \"C\" :0.46551724137931033,\n",
       "            \"T\" :0.07543103448275859,\n",
       "            \"G\" :0.3685344827586208\n",
       "        }\n",
       "    ],\n",
       "    \"name\" :\"DiscreteDistribution\"\n",
       "}], dtype=object)"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "gmm_2 = GeneralMixtureModel.from_json( gmm.to_json() )\n",
    "gmm_2.distributions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`gmm_2` is now ready to go, as if it was the original model!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Concluding Remarks"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "General Mixture Models are extremely powerful tools for unsupervised learning. pomegranate makes it easy to define, fit, and make predictions using these models. All the examples above have used the same distribution type for all the components of the mixture, but that is not a requirement, if your application requires varied distributions."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 2",
   "language": "python",
   "name": "python2"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
